#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>

#include "base/Configuration.h"

Configuration* global_cfg = NULL;

NEW_DELETE_OPERATORS_CPP(Configuration,1);

Configuration::Configuration (int *argc, const char** argv, const char** envp, char* config_file) :
    // we can't use the global Log, or have config option for this because of chicken/egg problem
    CfgLog("cfgLog", "", "stderr", NULL, NULL, false, false) 
{
    show_usage = false;
    arg_count = 0;
    const char* configfile;
    env_data = NULL;
    file_data = NULL;

    parseCommandLineArgs(argc, argv);
    readEnvironment(envp);

    // check for h/?/help with uArgExists so it doesn't show up in printArgs
    if (uArgExists("h") | uArgExists("?") | uArgExists("help")) {
        show_usage = true;
        CfgLog.i("Options can be specified as follows (in order of precedence): \n");
        CfgLog.i("On the command line like:\n");
        CfgLog.i("  progname <program_args> --- --<option> <value> --<option> <value> -<flag>\n\n");
        CfgLog.i("In the environment variables by setting the environment ");
        CfgLog.i("variable SIM_<option> to <value> (or anything for flags).\n\n");
        CfgLog.i("Or in the configuration file as '<option> <value>', one entry per line.\n\n");
        CfgLog.i("See Logger.h for a description of the syntax for log destinations.\n\n");
        CfgLog.i("Options are as follows:\n\n");
        CfgLog.i("%-25s %-50s %s\n","option","description", "default");
        CfgLog.i("%-25s %-50s %s\n","------","-----------", "-------");         

    }
    configfile = getStringArgument("configfile", "Configuration file name.", "config.sim");
    readConfigFile(configfile);

    // if configfile is in the configfile, mark it as used
    int n = findUArg("configfile");
    if (n >= 0) {
        uargs[n].used = true;
    }

}

Configuration::~Configuration () {
    if (env_data) FREE(env_data);
    if (file_data) FREE(file_data);
    for (int i=0; i<arg_count; i++) {
        FREE(args[i].name);
        if (args[i].type == ARG_STRING) {
            FREE(args[i].value);
        }
    }
}

void Configuration::printArgs (Logger* log, SEVERITY sev) {
    for (int i=0; i<arg_count; i++) {
        switch (args[i].type) {
        case ARG_BOOLEAN:
            log->o(sev, "%s %s\n", args[i].name, (bool)args[i].value ? "1" : "0");
            break;
        case ARG_INTEGER:
            log->o(sev, "%s %d\n", args[i].name, (int)args[i].value);
            break;
        case ARG_UNSIGNED:
            log->o(sev, "%s %d (0x%x)\n", args[i].name, (uint)args[i].value, (uint)args[i].value);
            break;
        case ARG_STRING:
            log->o(sev, "%s %s\n", args[i].name, (char*)args[i].value);
            break;
        }
    }
}

arg* Configuration::getArgs () {
    return args;
}

void Configuration::addUArg (int source, const char* name, const char* value, int line = -1) {
    if (!uArgExists(name)) {
        user_arg* arg = &uargs[uarg_count++];
        arg->source = source;
        arg->name = name;
        arg->value = value;
        arg->line = line;
        arg->used = false;
    }
}

// assumes that it doesn't already exist!
void Configuration::addArg (const char* name, ARG_TYPE type, void* value) {
    arg* arg = &args[arg_count++];
    arg->name = (char*)CALLOC(strlen(name)+1, sizeof(char));
    strcpy(arg->name, name);
    arg->type = type;
    arg->value = value;
}

int Configuration::findUArg (const char* name) {
    for (int i=0; i<uarg_count; i++) {
        if (strcmp(name, uargs[i].name) == 0) {
            return i;
        }
    }
    return -1;
}

bool Configuration::uArgExists(const char* name) {
    return (findUArg(name) >= 0);
}

int Configuration::findArg (const char* name) {
    for (int i=0; i<arg_count; i++) {
        if (strcmp(name, args[i].name) == 0) {
            return i;
        }
    }
    return -1;
}


bool Configuration::getBooleanArgument (const char* name, const char* desc, bool default_bool) {
    int arg_num = findArg(name);
    if (arg_num >= 0) {
        return (bool)args[arg_num].value;
    }
    int boolean = default_bool;
    getArgument(name, desc, ARG_BOOLEAN, &boolean, CONFIG_RANGE, 0, 1);
    addArg(name, ARG_BOOLEAN, (void*)boolean);
    return boolean;
}

int Configuration::getIntegerArgument (const char* name, const char* desc, int default_int,
                                       int flags, int lo, int hi) 
{
    int arg_num = findArg(name);
    if (arg_num >= 0) {
        return (int)args[arg_num].value;
    }
    int integer = default_int;
    getArgument(name, desc, ARG_INTEGER, &integer, flags, lo, hi);
    addArg(name, ARG_INTEGER, (void*)integer);
    return integer;
}

uint Configuration::getUnsignedArgument (const char* name, const char* desc, uint default_int,
                                         int flags, uint lo, uint hi) 
{
    int arg_num = findArg(name);
    if (arg_num >= 0) {
        return (int)args[arg_num].value;
    }
    uint integer = default_int;
    getArgument(name, desc, ARG_UNSIGNED, &integer, flags, lo, hi);
    addArg(name, ARG_UNSIGNED, (void*)integer);
    return integer;
}

const char* Configuration::getStringArgument (const char* name, const char* desc, const char* default_str) {
    int arg_num = findArg(name);
    if (arg_num >= 0) {
        return (char*)args[arg_num].value;
    }
    char* str = (char*)CALLOC(STR_BUFFER_SIZE, sizeof(char));
    strncpy(str, default_str, STR_BUFFER_SIZE);
    getArgument(name, desc, ARG_STRING, str);
    addArg(name, ARG_STRING, (void*)str);
    return str;
}

void Configuration::getArgument (const char* name,
                                 const char* desc,
                                 ARG_TYPE type,
                                 void* data,
                                 int flags,
                                 int lo, int hi)
{
    if (show_usage) {
        CfgLog.i("%-25s %-50s ", name, desc);
        switch (type) {
        case ARG_BOOLEAN:
            CfgLog.i("%s\n",*((bool*)data) ? "true" : "false");
            break;
        case ARG_INTEGER:
            CfgLog.i("%d\n",*((int*)data));
            break;
        case ARG_UNSIGNED:
            CfgLog.i("%d (0x%x)\n",*((uint*)data), *((uint*)data));
            break;
        case ARG_STRING:
            CfgLog.i("%s\n",(char*)(data));
            break;
        default:
            CfgLog.e("<UNKNOWN %d>\n", type);
        }
    }   
    
    int n = findUArg(name);
    if (n < 0) {
        // note we already set default in public function
        return;
    }
    uargs[n].used = true;
    switch (type) {
    case ARG_BOOLEAN:
    case ARG_INTEGER: {
        if (sscanf(uargs[n].value, "%i", (int*)data) != 1) {
            CfgLog.e("ERROR in %s, variable %s: unable to parse option value %s\n",
                     getSourceName(uargs[n].source), uargs[n].name, uargs[n].value);
        }
        break;
    }
    case ARG_UNSIGNED: {
        long long int i;
        ASSERT(sizeof(long long int) > sizeof(void*));
        if (sscanf(uargs[n].value, "%lli", &i) != 1) {
            CfgLog.e("ERROR in %s, variable %s: unable to parse option value %s\n",
                     getSourceName(uargs[n].source), uargs[n].name, uargs[n].value);
        }
        else {
            *(uint*)data = ADDR_UINT32(i);
        }
        break;
    }
    case ARG_STRING: {
        if (sscanf(uargs[n].value, "%s", (char*)data) != 1) {
            CfgLog.e("ERROR in %s, variable %s: unable to parse option value %s\n",
                     getSourceName(uargs[n].source), uargs[n].name, uargs[n].value);
        }
        break;
    }
    default: 
        CfgLog.e("ERROR in %s, variable %s: value %s: unknown argument type %d\n",
                 getSourceName(uargs[n].source), uargs[n].name, uargs[n].value, type);
    } 
    validateArg(&uargs[n], type, flags, lo, hi, data);
}

void Configuration::checkUnused () {
    if (show_usage) {
        exit(0);
    }
    bool all_used = true;
    for (int i=0; i<uarg_count; i++) {
        if (!uargs[i].used) {
            CfgLog.w("Argument %s given in %s, but not used.\n",
                     uargs[i].name, getSourceName(uargs[i].source));
            all_used = false;
        }
    }
    if (!all_used) {
        CfgLog.e("Exiting due to unused options.\n");
    }
}

void Configuration::validateArg (user_arg* arg, ARG_TYPE type, int flags, int lo, int hi, void* data) {
    if (flags & CONFIG_POW2) {
        int tmp = *((int*)data);
        if ((tmp == 0) || ((tmp & (tmp-1)) != 0)) {
            CfgLog.e("ERROR in %s, variable %s: value is not power of 2\n",
                   getSourceName(arg->source), arg->name);
        }
    }
    if (flags & CONFIG_UNSIGNED) {
        int tmp = *((int*)data);
        if (tmp < 0) {
            CfgLog.e("ERROR in %s, variable %s: value must be non-negative\n",
                   getSourceName(arg->source), arg->name);
        }
    }
    if (flags & CONFIG_RANGE) {
        int tmp = *((int*)data);
        if ((tmp < lo) || (tmp > hi)) {
            CfgLog.e("ERROR in %s, variable %s: value must be in range '%d' to '%d' (inclusive)\n",
                   getSourceName(arg->source), arg->name, 
                   lo, hi);
        }
    }
    if (flags & CONFIG_MIN) {
        int tmp = *((int*)data);
        if (tmp < lo) {
            CfgLog.e("ERROR in %s, variable %s: value must be equal to or above '%d' \n",
                   getSourceName(arg->source), arg->name,
                   lo);
        }
    }
}    

const char* Configuration::getSourceName (int source) {
    switch (source) {
    case SRC_CMDLINE: return("command line");
    case SRC_ENV: return("environment variables");
    case SRC_FILE: return("configuration file");
    }
    return("<unknown source>");
}

void Configuration::parseCommandLineArgs (int* argc, const char** argv) {
    const char* arg;
    int orig_argc = *argc;
    bool sim_args = false;
    for (int i=1; i<orig_argc; i++) {
        if (sim_args) { 
            arg = argv[i];
            if (*arg == '-') {
                if (*(arg+1) == '-') {
                    if (orig_argc <= i+1) {
                        CfgLog.e("Missing value for %s\n", arg);
                    }
                    arg += 2; // skip "--"
                    addUArg(SRC_CMDLINE, arg, argv[++i]);
                } else if (*(arg+1) == 'n' &&
                          *(arg+2) == 'o') {
                    arg += 3; // skip "-no"
                    addUArg(SRC_CMDLINE, arg, "0");

                } else {
                    arg += 1; // skip "-"
                    addUArg(SRC_CMDLINE, arg, "1");
                }
            } else {
                CfgLog.e("Invalid command line argument: %s\n", arg);
            }
        } else {
            if (strcmp("---", argv[i]) == 0) {
                sim_args = true;
                *argc = i;
            }
        }
    }
}

void Configuration::readEnvironment (const char** envp) {
    // look for environment variable overrides
    const char* envvar_prefix = "SIM_";
    uint envvar_prefix_len = strlen(envvar_prefix);
    const char* sim_root = "SIM_ROOT=";
    uint sim_root_len = strlen(sim_root);

    // see how much room we need
    int env_data_len = 0;
    for (const char** env = envp; *env != NULL; env++) {
        // see if variable starts with SIM_
        if (strncmp(*env, envvar_prefix, envvar_prefix_len) != 0) {
            continue;
        }
        // skip SIM_ROOT variable
        if (strncmp(*env, sim_root, sim_root_len) == 0) {
            continue;
        }
        env_data_len += strlen(*env) - envvar_prefix_len + 1; // plus one for extra null
    }

    env_data = (char*)CALLOC(env_data_len, sizeof(char));
    char* data = env_data;

    for (const char** env = envp; *env != NULL; env++) {
        // see if variable starts with SIM_
        if (strncmp(*env, envvar_prefix, envvar_prefix_len) != 0) {
            continue;
        }
        // skip SIM_ROOT variable
        if (strncmp(*env, sim_root, sim_root_len) == 0) {
            continue;
        }

        // skip SIM_ part to point to start of name
        strcpy(data, *env+envvar_prefix_len);
        char* eq_char = strchr(data, '=');
        *eq_char = '\0'; // null terminate the name string
        addUArg(SRC_ENV, data, data+strlen(data)+1);
        data += strlen(data) + 1; // move past name
        data += strlen(data) + 1; // move past value
    }
}    

void Configuration::readConfigFile (const char* configfile) {
    if (!configfile) {
        return;
    }
    FILE* f = fopen(configfile,"r");
    if (!f) {
        if (!show_usage) {
            CfgLog.d("Configuration file %s not found\n", configfile);
        }
        return;
    } else if (!show_usage) {
        CfgLog.i("Using configuration file %s.\n", configfile);
    }

    struct stat buf;
    int fstatResult = fstat(fileno(f), &buf);
    assert(fstatResult == 0);
    int file_data_len = buf.st_size+1; // extra for \0

    file_data = (char*)CALLOC(file_data_len, sizeof(char));
    char* data = file_data;

    int line = 1;
    int res;
    while ((res = fscanf(f, " %s", data)) != EOF) {    
        if (res != 1) {
            CfgLog.e("ERROR reading %s, line %d\n",configfile,line);
        }
        if (data[0] == '#') {
            // line comments
            res = fscanf(f,"[^\n]");
            continue;
        }
            
        char* name = data;
        data += strlen(name) + 1;

        if (fscanf(f, " %s\n", data) != 1) {
            *data = '\0';
        }
        addUArg(SRC_FILE, name, data, line);
        data += strlen(data) + 1;

        line++;
    }
    fclose(f);
}    

